/*==============================================================================

A viewer for neuron connectivity graphs.

Copyright (c) 2019 - 2024  David Brooks

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

==============================================================================*/

import cytoscape from 'cytoscape'
import dagre from 'cytoscape-dagre'
cytoscape.use( dagre );

//==============================================================================


//==============================================================================

const inArray = function (ar1, ar2) {
    if (!ar1 || !ar2) return false
    let as1 = JSON.stringify(ar1)
    let as2 = JSON.stringify(ar2)
    return as1.indexOf(as2) !== -1
}

const removeDuplicates = function (arrayOfAnything) {
    if (!arrayOfAnything) return []
    return [...new Set(arrayOfAnything.map((e) => JSON.stringify(e)))].map((e) =>
      JSON.parse(e)
    )
}

const findComponents = function (knowledge, axons, dendrites) {
    let dnodes = knowledge.connectivity.flat() // get nodes from edgelist
    let nodes = removeDuplicates(dnodes)

    let found = []
    let terminal = false
    nodes.forEach((node) => {
      terminal = false
      // Check if the node is an destination or origin (note that they are labelled dendrite and axon as opposed to origin and destination)
      if (inArray(axons, node)) {
        terminal = true
      }
      if (inArray(dendrites, node)) {
        terminal = true
      }
      if (!terminal) {
        found.push(node)
      }
    })

    return found
  }

export class ConnectivityGraph extends EventTarget
{
    cyg = null
    nodes = []
    edges = []
    axons = []
    dendrites = []
    somas = []
    labelCache = new Map()
    graphCanvas = null
    hasPhenotypes = false

    constructor(labelCache, graphCanvas)
    {
        super()
        this.labelCache = labelCache;
        this.graphCanvas = graphCanvas;
    }

    async addConnectivity(knowledge)
    //=====================================================
    {
        if (knowledge && knowledge["node-phenotypes"]) {
            const sourceKey = ["ilxtr:hasSomaLocatedIn"]
            const destinationKey = ["ilxtr:hasAxonPresynapticElementIn", "ilxtr:hasAxonSensorySubcellularElementIn"]

            const source = []
            const destination = []
            sourceKey.forEach((key)=>{
            source.push(...knowledge["node-phenotypes"][key])
            })
            destinationKey.forEach((key)=>{
            destination.push(...knowledge["node-phenotypes"][key])
            })
            const via = findComponents(knowledge, source, destination)
            this.dendrites = source.map(node => JSON.stringify(node))
            this.axons = destination.map(node => JSON.stringify(node))
            if (via?.length) {
                this.somas = via.map(node => JSON.stringify(node))
            }
            this.hasPhenotypes = true
        } else {
            this.axons = knowledge.axons.map(node => JSON.stringify(node))
            this.dendrites = knowledge.dendrites.map(node => JSON.stringify(node))
            if (knowledge.somas?.length) {
                this.somas = knowledge.somas.map(node => JSON.stringify(node))
            }
            this.hasPhenotypes = false
        }
        if (knowledge.connectivity.length) {
            for (const edge of knowledge.connectivity) {
                const e0 = await this.graphNode(edge[0])
                const e1 = await this.graphNode(edge[1])
                this.nodes.push(e0)
                this.nodes.push(e1)
                this.edges.push({
                    id: `${e0.id}_${e1.id}`,
                    source: e0.id,
                    target: e1.id
                })
            }
        } else {
            this.nodes.push({
                id: 'MISSING',
                label: 'NO PATHS'
            })
        }
    }

    showConnectivity(graphCanvas)
    //================
    {
        this.cyg = new CytoscapeGraph(this, graphCanvas)

        this.cyg.on('tap-node', (event) => {
            const tapEvent = new CustomEvent('tap-node', {
                detail: event.detail
            })
            this.dispatchEvent(tapEvent);
        });
    }

    selectConnectivity(selectedConnectivityData)
    {
        if (this.cyg?.cy) {
            let eleId = ''
            this.cyg.cy.elements().forEach((ele) => {
                const label = ele.data('label')
                const connectivityData = getConnectivityData(label)

                if (areArraysIdentical(selectedConnectivityData, connectivityData)) {
                    eleId = ele.id()
                }
            })

            if (eleId) {
                this.cyg.cy.$id(eleId).select()
            }
        }
    }

    clearConnectivity()
    //=================
    {
        if (this.cyg?.cy) {
            this.cyg.cy.remove()
            this.cyg.cy = null
        }
    }

    reset()
    //=====
    {
        if (this.cyg?.cy) {
            this.cyg.cy.reset()
        }
    }

    zoom(val)
    //=======
    {
        if (this.cyg?.cy) {
            const currentZoom = this.cyg.cy.zoom()
            const width = this.cyg.cy.width()
            const height = this.cyg.cy.height()
            const positionToRender = {
                x: width/2,
                y: height/2,
            }
            this.cyg.cy.zoom({
                level: currentZoom + val,
                renderedPosition: positionToRender,
            })
        }
    }

    enableZoom(option)
    //================
    {
        if (this.cyg?.cy) {
            this.cyg.cy.userZoomingEnabled(option)
        }
    }

    get elements()
    //============
    {
        return [
            ...this.nodes.map(n => { return {data: n}}),
            ...this.edges.map(e => { return {data: e}})
        ]
    }

    get roots()
    //===================
    {
        if (this.hasPhenotypes) {
            return [
                ...this.dendrites,
            ]
        } else {
            return [
                ...this.dendrites,
                ...this.somas
            ]
        }

    }

    async graphNode(node)
    //=======================================================
    {
        const id = JSON.stringify(node)
        const label = [node[0], ...node[1]]
        const humanLabels = []
        for (const term of label) {
            const humanLabel = this.labelCache.has(term) ? this.labelCache.get(term) : ''
            humanLabels.push(humanLabel)
        }
        label.push(...humanLabels)

        const result = {
            id,
            label: label.join('\n')
        }
        if (this.hasPhenotypes) {
            if (this.axons.includes(id)) {
                result['axon'] = true
            } else if (this.dendrites.includes(id)) {
                result['dendrite'] = true
            } else {
                result['somas'] = true
            }
        } else {
            if (this.axons.includes(id)) {
                if (this.dendrites.includes(id) || this.somas.includes(id)) {
                    result['somas'] = true
                } else {
                    result['axon'] = true
                }
            } else if (this.dendrites.includes(id) || this.somas.includes(id)) {
                result['dendrite'] = true

            }
        }
        return result
    }

    on(eventName, callback)
    //=====================
    {
        this.addEventListener(eventName, callback)
    }
}

//==============================================================================

const APP_PRIMARY_COLOR = '#8300bf'
const BG_COLOR = '#f3ecf6'
const GRAPH_STYLE = [
    {
        'selector': 'node',
        'style': {
            'label': function(ele) { return trimLabel(ele.data('label')) },
            // 'background-color': '#80F0F0',
            'background-color': 'transparent',
            'background-opacity': '0',
            'text-valign': 'center',
            'text-wrap': 'wrap',
            'width': '80px',
            'height': '80px',
            'text-max-width': '80px',
            'font-size': '6px',
            'shape': 'round-rectangle',
            'border-width': 1,
            'border-style': 'solid',
            'border-color': 'gray',
        }
    },
    {
        'selector': 'node[axon]',
        'style': {
            // 'background-color': 'green',
            'shape': 'round-diamond',
            'width': '100px',
            'height': '100px',
        }
    },
    {
        'selector': 'node[dendrite]',
        'style': {
            // 'background-color': 'red',
            'shape': 'ellipse',
        }
    },
    {
        'selector': 'node[somas]',
        'style': {
            // 'background-color': 'gray',
            'shape': 'round-rectangle',
        }
    },
    {
        'selector': 'edge',
        'style': {
            'width': 1,
            'line-color': 'dimgray',
            'curve-style': 'bezier'
        }
    },
    {
        'selector': 'node.active',
        'style': {
            'border-color': APP_PRIMARY_COLOR,
            'background-color': BG_COLOR,
            'background-opacity': 0.75,
        }
    }
]

function trimLabel(label) {
    const labels = label.split('\n')
    const half = labels.length/2
    const trimLabels = labels.slice(half)
    return capitalizeLabels(trimLabels.join('\n'))
}

function capitalizeLabels(input) {
    return input.split('\n').map(label => {
        if (label && label[0] >= 'a' && label[0] <= 'z') {
            return label.charAt(0).toUpperCase() + label.slice(1)
        }
        return label
    }).join('\n')
}

function getConnectivityData(label) {
    const labels = label ? label.split(`\n`) : []
    const connectivityData = []

    for (let i = 0; i < labels.length / 2; i++) {
        connectivityData.push({
            id: labels[i],
            label: labels[i + labels.length / 2]
        })
    }
    return connectivityData
}

function areArraysIdentical(arr1, arr2) {
    arr1.sort((a, b) => {
      if (a.id < b.id) return -1
      if (a.id > b.id) return 1
      return 0
    })

    arr2.sort((a, b) => {
      if (a.id < b.id) return -1
      if (a.id > b.id) return 1
      return 0
    })

    for (let i = 0; i < arr1.length; i++) {
      if (JSON.stringify(arr1[i]) !== JSON.stringify(arr2[i])) {
        return false
      }
    }

    return true
}
//==============================================================================

class CytoscapeGraph extends EventTarget
{
    cy
    tooltip

    constructor(connectivityGraph, graphCanvas)
    {
        super()
        this.cy = cytoscape({
            container: graphCanvas,
            elements: connectivityGraph.elements,
            layout: {
                name: 'dagre',
                nodeSep: 150,
                edgeSep: 50,
                rankSep: 100,
                rankDir: 'TB',
                roots: connectivityGraph.roots.length ? connectivityGraph.roots : undefined,
            },
            style: GRAPH_STYLE,
            minZoom: 0.1,
            maxZoom: 10,
            wheelSensitivity: 0.4,
        }).on('mouseover', 'node', this.overNode.bind(this))
          .on('mouseout', 'node', this.exitNode.bind(this))
          .on('position', 'node', this.moveNode.bind(this))

        this.tooltip = document.createElement('div')
        this.tooltip.className = 'cy-graph-tooltip'
        this.tooltip.hidden = true
        graphCanvas?.lastChild?.appendChild(this.tooltip)
    }

    remove()
    //======
    {
        if (this.cy) {
            this.cy.destroy()
        }
    }

    checkRightBoundary(leftPos)
    //==================================
    {
        if ((leftPos + this.tooltip.offsetWidth) >= this.tooltip.parentElement?.offsetWidth) {
            this.tooltip.style.left = `${leftPos - this.tooltip.offsetWidth}px`
        }
    }

    overNode(event)
    //==============
    {
        const node = event.target
        const data = node.data()
        const { label } = data
        const connectivityData = getConnectivityData(label)
        const labels = connectivityData.map((data) => (
            data.label + ' (' + data.id + ')'
        ))

        this.tooltip.innerText = capitalizeLabels(labels.join('\n'))
        this.tooltip.style.left = `${event.renderedPosition.x}px`
        this.tooltip.style.top = `${event.renderedPosition.y}px`
        this.tooltip.style.maxWidth = '240px'
        this.tooltip.style.zIndex = 2
        this.tooltip.hidden = false

        this.checkRightBoundary(event.renderedPosition.x)

        this.tapNode(event, true)
    }

    moveNode(event)
    //==============
    {
        const node = event.target
        this.tooltip.style.left = `${node.renderedPosition().x}px`
        this.tooltip.style.top = `${node.renderedPosition().y}px`
        this.checkRightBoundary(node.renderedPosition().x)
    }

    exitNode(event)
    //==============
    {
        this.tooltip.hidden = true

        this.tapNode(event, false)
    }

    tapNode(event, show)
    //============
    {
        const node = event.target
        const data = node.data()
        let { label } = data

        if (show) {
            node.addClass('active')
        } else {
            node.removeClass('active')
            label = ''
            setTimeout(() => {
                node.unselect()
            })
        }

        const connectivityData = getConnectivityData(label)

        const tapEvent = new CustomEvent('tap-node', {
            detail: connectivityData
        })
        this.dispatchEvent(tapEvent);
    }

    on(eventName, callback)
    //=====================
    {
        this.addEventListener(eventName, callback)
    }
}

//==============================================================================
